/*
Copyright (C) 2011 The University of Michigan

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.

Please send inquiries to powertutor@umich.edu
 */

package vn.cybersoft.obs.andriod.batterystats2.util;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

import android.app.ActivityManager;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.graphics.drawable.Drawable;
import android.os.Process;
import android.os.SystemClock;
import android.util.Log;
import android.util.SparseArray;

public class SystemInfo {
	private static final String TAG = "SystemInfo";
	private static SystemInfo instance = new SystemInfo();

	public static SystemInfo getInstance() {
		return instance;
	}

	/* Uids as listed in android_filesystem_config.h */
	public static final int AID_ALL         =   -1; /* A special constant we will
	 * use to indicate a request
	 * for global information. */
	public static final int AID_ROOT        =    0; /* traditional unix root user
	 */
	public static final int AID_SYSTEM      = 1000; /* system server */
	public static final int AID_RADIO       = 1001; /* telephony subsystem, RIL */
	public static final int AID_BLUETOOTH   = 1002; /* bluetooth subsystem */
	public static final int AID_GRAPHICS    = 1003; /* graphics devices */
	public static final int AID_INPUT       = 1004; /* input devices */
	public static final int AID_AUDIO       = 1005; /* audio devices */
	public static final int AID_CAMERA      = 1006; /* camera devices */
	public static final int AID_LOG         = 1007; /* log devices */
	public static final int AID_COMPASS     = 1008; /* compass device */
	public static final int AID_MOUNT       = 1009; /* mountd socket */
	public static final int AID_WIFI        = 1010; /* wifi subsystem */
	public static final int AID_ADB         = 1011; /* android debug bridge
                                                     (adbd) */
	public static final int AID_INSTALL     = 1012; /* group for installing
                                                     packages */
	public static final int AID_MEDIA       = 1013; /* mediaserver process */
	public static final int AID_DHCP        = 1014; /* dhcp client */
	public static final int AID_SHELL       = 2000; /* adb and debug shell user */
	public static final int AID_CACHE       = 2001; /* cache access */
	public static final int AID_DIAG        = 2002; /* access to diagnostic
                                                     resources */
	/* The 3000 series are intended for use as supplemental group id's only.
	 * They indicate special Android capabilities that the kernel is aware of. */
	public static final int AID_NET_BT_ADMIN= 3001; /* bluetooth: create any
                                                     socket */
	public static final int AID_NET_BT      = 3002; /* bluetooth: create sco,
                                                     rfcomm or l2cap sockets */
	public static final int AID_INET        = 3003; /* can create AF_INET and
                                                     AF_INET6 sockets */
	public static final int AID_NET_RAW     = 3004; /* can create raw INET sockets
	 */
	public static final int AID_MISC        = 9998; /* access to misc storage */
	public static final int AID_NOBODY      = 9999;
	public static final int AID_APP         =10000; /* first app user */

	/* These are stolen from Process.java which hides these constants. */
	public static final int PROC_SPACE_TERM = (int)' ';
	public static final int PROC_TAB_TERM = (int)'\t';
	public static final int PROC_LINE_TERM = (int)'\n';
	public static final int PROC_COMBINE = 0x100;
	public static final int PROC_OUT_LONG = 0x2000;
	private static final int[] READ_LONG_FORMAT = new int[] {
		PROC_SPACE_TERM|PROC_OUT_LONG
	};
	private static final int[] PROCESS_STATS_FORMAT = new int[] {
		PROC_SPACE_TERM,
		PROC_SPACE_TERM,
		PROC_SPACE_TERM,
		PROC_SPACE_TERM,
		PROC_SPACE_TERM,
		PROC_SPACE_TERM,
		PROC_SPACE_TERM,
		PROC_SPACE_TERM,
		PROC_SPACE_TERM,
		PROC_SPACE_TERM,
		PROC_SPACE_TERM,
		PROC_SPACE_TERM,
		PROC_SPACE_TERM,
		PROC_SPACE_TERM|PROC_OUT_LONG,                  // 13: utime
		PROC_SPACE_TERM|PROC_OUT_LONG                   // 14: stime
	};
	private static final int[] PROCESS_TOTAL_STATS_FORMAT = new int[] {
		PROC_SPACE_TERM,
		PROC_SPACE_TERM|PROC_OUT_LONG,
		PROC_SPACE_TERM|PROC_OUT_LONG,
		PROC_SPACE_TERM|PROC_OUT_LONG,
		PROC_SPACE_TERM|PROC_OUT_LONG,
		PROC_SPACE_TERM|PROC_OUT_LONG,
		PROC_SPACE_TERM|PROC_OUT_LONG,
		PROC_SPACE_TERM|PROC_OUT_LONG,
	};
	private static final int[] PROC_MEMINFO_FORMAT = new int[] {
		PROC_SPACE_TERM|PROC_COMBINE, PROC_SPACE_TERM|PROC_OUT_LONG, PROC_LINE_TERM,
		PROC_SPACE_TERM|PROC_COMBINE, PROC_SPACE_TERM|PROC_OUT_LONG, PROC_LINE_TERM,
		PROC_SPACE_TERM|PROC_COMBINE, PROC_SPACE_TERM|PROC_OUT_LONG, PROC_LINE_TERM,
		PROC_SPACE_TERM|PROC_COMBINE, PROC_SPACE_TERM|PROC_OUT_LONG, PROC_LINE_TERM,
	};

	public static final int INDEX_USER_TIME = 0;
	public static final int INDEX_SYS_TIME = 1;
	public static final int INDEX_TOTAL_TIME = 2;

	public static final int INDEX_MEM_TOTAL = 0;
	public static final int INDEX_MEM_FREE = 1;
	public static final int INDEX_MEM_BUFFERS = 2;
	public static final int INDEX_MEM_CACHED = 3;

	/* We are going to take advantage of the hidden API within Process.java that
	 * makes use of JNI so that we can perform the top task efficiently.
	 */
	private Field fieldUid;
	private Method methodGetUidForPid;
	private Method methodGetPids;
	private Method methodReadProcFile;
	private Method methodGetProperty;

	private long[] readBuf;

	private SystemInfo() {
		try {
			fieldUid = ActivityManager.RunningAppProcessInfo.class.getField("uid");
		} catch(NoSuchFieldException e) {
			/* API level 3 doesn't have this field unfortunately. */
		}
		try {
			methodGetUidForPid = Process.class.getMethod("getUidForPid", int.class);
		} catch(NoSuchMethodException e) {
			Log.w(TAG, "Could not access getUidForPid method");
		}
		try {
			methodGetPids = Process.class.getMethod("getPids", String.class,
					int[].class);
		} catch(NoSuchMethodException e) {
			Log.w(TAG, "Could not access getPids method");
		}
		try {
			methodReadProcFile = Process.class.getMethod("readProcFile", String.class,
					int[].class, String[].class, long[].class, float[].class);
		} catch(NoSuchMethodException e) {
			Log.w(TAG, "Could not access readProcFile method");
		}
		try {
			Class<?> classSystemProperties = Class.forName("android.os.SystemProperties");
			methodGetProperty = classSystemProperties.getMethod("get", String.class);
		} catch(NoSuchMethodException e) {
			Log.w(TAG, "Could not access SystemProperties.get");
		} catch(ClassNotFoundException e) {
			Log.w(TAG, "Could not find class android.os.SystemProperties");
		}
		readBuf = new long[1];
	}

	public int getUidForPid(int pid) {
		if(methodGetUidForPid != null) try {
			return (Integer)methodGetUidForPid.invoke(null, pid);
		} catch(InvocationTargetException e) {
			Log.w(TAG, "Call to getUidForPid failed");
		} catch(IllegalAccessException e) {
			Log.w(TAG, "Call to getUidForPid failed");
		} else try {
			BufferedReader rdr = new BufferedReader(new InputStreamReader(
					new FileInputStream("/proc/" + pid + "/status")), 256);
			for(String line = rdr.readLine(); line != null; line = rdr.readLine()) {
				if(line.startsWith("Uid:")) {
					String tokens[] = line.substring(4).split("[ \t]+"); 
					String realUidToken = tokens[tokens[0].length() == 0 ? 1 : 0];
					try {
						return Integer.parseInt(realUidToken);
					} catch(NumberFormatException e) {
						return -1;
					}
				}
			}
		} catch(IOException e) {
			Log.w(TAG, "Failed to manually read in process uid");
		} 
		return -1;
	}

	public int getUidForProcessInfo(
			ActivityManager.RunningAppProcessInfo app) {
		/* Try to access the uid field first if it is avaialble. Otherwise just
		 * convert the pid to a uid.
		 */
		if(fieldUid != null) try {
			return (Integer)fieldUid.get(app);
		} catch(IllegalAccessException e) {
		}
		return getUidForPid(app.pid);
	}

	/* lastPids can be null.  It is just used to avoid memory reallocation if
	 * at all possible. Returns null on failure. If lastPids can hold the new
	 * pid list the extra entries will be filled with -1 at the end.
	 */
	public int[] getPids(int[] lastPids) {
		if(methodGetPids == null) return manualGetInts("/proc", lastPids);
		try {
			return (int[])methodGetPids.invoke(null, "/proc", lastPids);
		} catch(IllegalAccessException e) {
			Log.w(TAG, "Failed to get process cpu usage");
		} catch(InvocationTargetException e) {
			Log.w(TAG, "Exception thrown while getting cpu usage");
		}
		return null;
	}

	/* Gets a property on Android accessible through getprop. */
	public String getProperty(String property) {
		if(methodGetProperty == null) return null;
		try {
			return (String)methodGetProperty.invoke(null, property);
		} catch(IllegalAccessException e) {
			Log.w(TAG, "Failed to get property");
		} catch(InvocationTargetException e) {
			Log.w(TAG, "Exception thrown while getting property");
		}
		return null;
	}

	/* lastUids can be null.  It is just used to avoid memory reallocation if
	 * at all possible. Returns null on failure. If lastUids can hold the new
	 * uid list the extra entries will be filled with -1 at the end.
	 */
	public int[] getUids(int[] lastUids) {
		if(methodGetPids == null) return manualGetInts("/proc/uid_stat", lastUids);
		try {
			return (int[])methodGetPids.invoke(null, "/proc/uid_stat", lastUids);
		} catch(IllegalAccessException e) {
			Log.w(TAG, "Failed to get process cpu usage");
		} catch(InvocationTargetException e) {
			Log.w(TAG, "Exception thrown while getting cpu usage");
		}
		return null;
	}

	private int[] manualGetInts(String dir, int[] lastInts) {
		File[] files = new File(dir).listFiles();
		int sz = files == null ? 0 : files.length;
		if(lastInts == null || lastInts.length < sz) {
			lastInts = new int[sz];
		} else if(2 * sz < lastInts.length) {
			lastInts = new int[sz];
		}
		int pos = 0;
		for(int i = 0; i < sz; i++) {
			try {
				int v = Integer.parseInt(files[i].getName());
				lastInts[pos++] = v;
			} catch(NumberFormatException e) {
			}
		}
		while(pos < lastInts.length) lastInts[pos++] = -1;
		return lastInts;
	}

	/* times should contain two elements.  times[INDEX_USER_TIME] will be filled
	 * with the user time for this pid and times[INDEX_SYS_TIME] will be filled
	 * with the sys time for this pid.  Returns true on sucess.
	 */
	public boolean getPidUsrSysTime(int pid, long[] times) {
		if(methodReadProcFile == null) return false;
		try {
			return (Boolean)methodReadProcFile.invoke(
					null, "/proc/" + pid + "/stat",
					PROCESS_STATS_FORMAT, null, times, null);
		} catch(IllegalAccessException e) {
			Log.w(TAG, "Failed to get pid cpu usage");
		} catch(InvocationTargetException e) {
			Log.w(TAG, "Exception thrown while getting pid cpu usage");
		}
		return false;
	}

	/* times should contain seven elements.  times[INDEX_USER_TIME] will be filled
	 * with the total user time, times[INDEX_SYS_TIME] will be filled
	 * with the total sys time, and times[INDEX_TOTAL_TIME] will have the total
	 * time (including idle cycles).  Returns true on success.
	 */
	public boolean getUsrSysTotalTime(long[] times) {
		if(methodReadProcFile == null) return false;
		try {
			if((Boolean)methodReadProcFile.invoke(
					null, "/proc/stat",
					PROCESS_TOTAL_STATS_FORMAT, null, times, null)) {
				long usr = times[0] + times[1];
				long sys = times[2] + times[5] + times[6];
				long total = usr + sys + times[3] + times[4];
				times[INDEX_USER_TIME] = usr;
				times[INDEX_SYS_TIME] = sys;
				times[INDEX_TOTAL_TIME] = total;
				return true;
			}
		} catch(IllegalAccessException e) {
			Log.w(TAG, "Failed to get total cpu usage");
		} catch(InvocationTargetException e) {
			Log.w(TAG, "Exception thrown while getting total cpu usage");
		}
		return false;
	}

	/* mem should contain 4 elements.  mem[INDEX_MEM_TOTAL] will contain total
	 * memory available in kb, mem[INDEX_MEM_FREE] will give the amount of free
	 * memory in kb, mem[INDEX_MEM_BUFFERS] will give the size of kernel buffers
	 * in kb, and mem[INDEX_MEM_CACHED] will give the size of kernel caches in kb.
	 * Returns true on success.
	 */
	public boolean getMemInfo(long[] mem) {
		if(methodReadProcFile == null) return false;
		try {
			if((Boolean)methodReadProcFile.invoke(
					null, "/proc/meminfo",
					PROC_MEMINFO_FORMAT, null, mem, null)) {
				return true;
			}
		} catch(IllegalAccessException e) {
			Log.w(TAG, "Failed to get mem info");
		} catch(InvocationTargetException e) {
			Log.w(TAG, "Exception thrown while getting mem info");
		}
		return false;
	}

	/* Returns -1 on failure. */
	public long readLongFromFile(String file) {
		if(methodReadProcFile == null) return -1;
		try {
			if((Boolean)methodReadProcFile.invoke(
					null, file, READ_LONG_FORMAT, null, readBuf, null)) {
				return readBuf[0];
			}
		} catch(IllegalAccessException e) {
			Log.w(TAG, "Failed to get pid cpu usage");
		} catch(InvocationTargetException e) {
			Log.w(TAG, "Exception thrown while getting pid cpu usage");
		}
		return -1L;
	}

	SparseArray<UidCacheEntry> uidCache = new SparseArray<UidCacheEntry>();

	public synchronized String getAppId(int uid, PackageManager pm) {
		UidCacheEntry cacheEntry = uidCache.get(uid);
		if(cacheEntry == null) {
			cacheEntry = new UidCacheEntry();
			uidCache.put(uid, cacheEntry);
		}
		cacheEntry.clearIfExpired();
		if(cacheEntry.getAppId() != null) {
			return cacheEntry.getAppId();
		}
		String result = getAppIdNoCache(uid, pm);
		cacheEntry.setAppId(result);
		return result;
	}

	private String getAppIdNoCache(int uid, PackageManager pm) {
		if(uid < SystemInfo.AID_APP) {
			Log.e(TAG, "Only pass application uids to getAppId");
			return null;
		}
		int versionCode = -1;
		String[] packages = pm.getPackagesForUid(uid);
		if(packages != null) for(String packageName : packages) {
			try {
				PackageInfo info = pm.getPackageInfo(packageName, 0);
				versionCode = info.versionCode;
			} catch(PackageManager.NameNotFoundException e) {
			}
		}
		String name = pm.getNameForUid(uid);
		name = name == null ? "none" : name;
		return pm.getNameForUid(uid) + "@" + versionCode;
	}

	public synchronized String getUidName(int uid, PackageManager pm) {
		UidCacheEntry cacheEntry = uidCache.get(uid);
		if(cacheEntry == null) {
			cacheEntry = new UidCacheEntry();
			uidCache.put(uid, cacheEntry);
		}
		cacheEntry.clearIfExpired();
		if(cacheEntry.getName() != null) {
			return cacheEntry.getName();
		}
		String result = getUidNameNoCache(uid, pm);
		cacheEntry.setName(result);
		return result;
	}

	private String getUidNameNoCache(int uid, PackageManager pm) {
		switch(uid) {
		case AID_ROOT:
			return "Kernel";
		case AID_SYSTEM:
			return "System";
		case AID_RADIO:
			return "Radio Subsystem";
		case AID_BLUETOOTH:
			return "Bluetooth Subsystem";
		case AID_GRAPHICS:
			return "Graphics Devices";
		case AID_INPUT:
			return "Input Devices";
		case AID_AUDIO:
			return "Audio Devices";
		case AID_CAMERA:
			return "Camera Devices"; case AID_LOG:
				return "Log Devices";
			case AID_COMPASS:
				return "Compass Device (e.g. akmd)";
			case AID_MOUNT:
				return "Mount";
			case AID_WIFI:
				return "Wifi Subsystem";
			case AID_ADB:
				return "Android Debug Bridge";
			case AID_INSTALL:
				return "Install";
			case AID_MEDIA:
				return "Media Server";
			case AID_DHCP:
				return "DHCP Client";
			case AID_SHELL:
				return "Debug Shell";
			case AID_CACHE:
				return "Cache Access";
			case AID_DIAG:
				return "Diagnostics";
		}
		if(uid < AID_APP) {
			return "sys_" + uid;
		}

		String[] packages = pm.getPackagesForUid(uid);
		if(packages != null) for(String packageName : packages) {
			try {
				PackageInfo info = pm.getPackageInfo(packageName, 0);
				CharSequence label = info.applicationInfo.loadLabel(pm);
				if(label != null) {
					return label.toString();
				}
			} catch(PackageManager.NameNotFoundException e) {
			}
		}
		String uidName = pm.getNameForUid(uid);
		if(uidName != null) {
			return uidName;
		}
		return "app_" + uid;
	}

	public synchronized Drawable getUidIcon(int uid, PackageManager pm) {
		UidCacheEntry cacheEntry = uidCache.get(uid);
		if(cacheEntry == null) {
			cacheEntry = new UidCacheEntry();
			uidCache.put(uid, cacheEntry);
		}
		cacheEntry.clearIfExpired();
		if(cacheEntry.getIcon() != null) {
			return cacheEntry.getIcon();
		}
		Drawable result = getUidIconNoCache(uid, pm);
		cacheEntry.setIcon(result);
		return result;
	}

	public Drawable getUidIconNoCache(int uid, PackageManager pm) {
		String[] packages = pm.getPackagesForUid(uid);
		if(packages != null) for (int i = 0; i < packages.length; i++) {
			try {
				ApplicationInfo ai = pm.getApplicationInfo(packages[i], 0);
				if(ai.icon != 0) {
					return ai.loadIcon(pm);
				}
			} catch(PackageManager.NameNotFoundException e) {
			}
		}
		return pm.getDefaultActivityIcon();
	}

	public synchronized void voidUidCache(int uid) {
		uidCache.remove(uid);
	}

	private static class UidCacheEntry {
		private static long EXPIRATION_TIME = 1000 * 60 * 10; // 10 minutes

		private String appId;
		private String name;
		private Drawable icon;
		private long updateTime;

		public UidCacheEntry() {
			updateTime = -1;
		}

		public String getAppId() {
			return appId;
		}

		public void setAppId(String appId) {
			this.appId = appId;
		}

		public String getName() {
			return name;
		}

		public void setName(String name) {
			this.name = name;
			if(updateTime == -1) {
				updateTime = SystemClock.elapsedRealtime();
			}
		}

		public Drawable getIcon() {
			return icon;
		}

		public void setIcon(Drawable icon) {
			this.icon = icon;
			if(updateTime == -1) {
				updateTime = SystemClock.elapsedRealtime();
			}
		}

		public void clearIfExpired() {
			if(updateTime != -1 &&
					updateTime + EXPIRATION_TIME < SystemClock.elapsedRealtime()) {
				updateTime = -1;
				name = null;
				icon = null;
			}
		}
	}
}

